/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.lucene.analysis;

import java.io.IOException;
import java.io.Reader;
import java.util.LinkedList;

/**
 * Simplistic {@link CharFilter} that applies the mappings
 * contained in a {@link NormalizeCharMap} to the character
 * stream, and correcting the resulting changes to the
 * offsets.
 */
public class MappingCharFilter extends BaseCharFilter {

    private final NormalizeCharMap normMap;
    private LinkedList<Character> buffer;
    private String replacement;
    private int charPointer;
    private int nextCharCounter;

    /** Default constructor that takes a {@link CharStream}. */
    public MappingCharFilter(NormalizeCharMap normMap, CharStream in) {
        super(in);
        this.normMap = normMap;
    }

    /** Easy-use constructor that takes a {@link Reader}. */
    public MappingCharFilter(NormalizeCharMap normMap, Reader in) {
        super(CharReader.get(in));
        this.normMap = normMap;
    }

    @Override
    public int read() throws IOException {
        while (true) {
            if (replacement != null && charPointer < replacement.length()) {
                return replacement.charAt(charPointer++);
            }

            int firstChar = nextChar();
            if (firstChar == -1)
                return -1;
            NormalizeCharMap nm = normMap.submap != null ? normMap.submap.get(Character.valueOf((char) firstChar)) : null;
            if (nm == null)
                return firstChar;
            NormalizeCharMap result = match(nm);
            if (result == null)
                return firstChar;
            replacement = result.normStr;
            charPointer = 0;
            if (result.diff != 0) {
                int prevCumulativeDiff = getLastCumulativeDiff();
                if (result.diff < 0) {
                    for (int i = 0; i < -result.diff; i++)
                        addOffCorrectMap(nextCharCounter + i - prevCumulativeDiff, prevCumulativeDiff - 1 - i);
                } else {
                    addOffCorrectMap(nextCharCounter - result.diff - prevCumulativeDiff, prevCumulativeDiff + result.diff);
                }
            }
        }
    }

    private int nextChar() throws IOException {
        nextCharCounter++;
        if (buffer != null && !buffer.isEmpty()) {
            return buffer.removeFirst().charValue();
        }
        return input.read();
    }

    private void pushChar(int c) {
        nextCharCounter--;
        if (buffer == null)
            buffer = new LinkedList<Character>();
        buffer.addFirst(Character.valueOf((char) c));
    }

    private void pushLastChar(int c) {
        if (buffer == null) {
            buffer = new LinkedList<Character>();
        }
        buffer.addLast(Character.valueOf((char) c));
    }

    private NormalizeCharMap match(NormalizeCharMap map) throws IOException {
        NormalizeCharMap result = null;
        if (map.submap != null) {
            int chr = nextChar();
            if (chr != -1) {
                NormalizeCharMap subMap = map.submap.get(Character.valueOf((char) chr));
                if (subMap != null) {
                    result = match(subMap);
                }
                if (result == null) {
                    pushChar(chr);
                }
            }
        }
        if (result == null && map.normStr != null) {
            result = map;
        }
        return result;
    }

    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
        char[] tmp = new char[len];
        int l = input.read(tmp, 0, len);
        if (l != -1) {
            for (int i = 0; i < l; i++)
                pushLastChar(tmp[i]);
        }
        l = 0;
        for (int i = off; i < off + len; i++) {
            int c = read();
            if (c == -1)
                break;
            cbuf[i] = (char) c;
            l++;
        }
        return l == 0 ? -1 : l;
    }
}
